package com.mintyfresh.tns.server;

import com.mintyfresh.tns.util.*;

import java.lang.*;
import java.net.*;
import java.io.*;
import java.util.*;
import static java.lang.System.*;

public class Server {
	private static HashMap<Long, Player> players = new HashMap<Long, Player>();
	private static HashMap<Long, ServerListener> sockets
		= new HashMap<Long, ServerListener>();
	private static int turnTimeout;
	private static int maxPlayers;
	private static int numRounds;
	private static Properties properties = new Properties();
	private static int connectedPlayerCount = 0;
	private static int port;
	private static int agents;

	private static int currentPower;
	private static int currentAngle;
	private static Player currentPlayer;
	private static Long currentSession;
	private static int currentRound;
	private static boolean playerNotShot;
	private static HashMap<Long, Player> roundPlayers;

	public static void main(String[] args) throws IOException {
		ServerSocket serverSocket = null;
		try {
			port = Integer.parseInt(args[0]);
			maxPlayers = Integer.parseInt(args[1]);
			numRounds = Integer.parseInt(args[2]);
			turnTimeout = Integer.parseInt(args[3]);
			agents = Integer.parseInt(args[4]);

			serverSocket = new ServerSocket(port);
			System.out.println(
					"Server is up and running. Listening on port "+port);
		} catch (IOException ioe) {
			System.err.println("Could not listen on port: " + port);
			printUsage();
			System.exit(-1);
		} catch (Exception e){
			System.err.println("Server failed to start: ");
			e.printStackTrace();
			printUsage();
			System.exit(-1);
		}

		// Open sockets for players
		while(sockets.size() < (maxPlayers-agents)) {
			long nextSession = System.currentTimeMillis();
			ServerListener sl
				= new ServerListener(serverSocket.accept(), nextSession);
			sl.start();
			sockets.put(new Long(nextSession), sl);
		}

		// Connect agents
		synchronized (players) {
			boolean isAgent = true;
			for (int i = 1; i <= agents; i++) {
				players.put(
						new Long(-i),
						new Player("Agent"+i, null, isAgent));
				connectedPlayerCount++;
			}
		}

		// Wait for players to connect
		while(connectedPlayerCount < maxPlayers) {}

		// Run all the rounds
		for(currentRound = 1; currentRound <= numRounds; currentRound++) {
			send(runRound());
			// Pause for a second to let the clients breathe
			try { Thread.sleep(1000); }
			catch (InterruptedException e) { e.printStackTrace(); } 
		}
		endGame();
	}

	public static void newPlayer(
			long session, String username, ObjectOutputStream out)
	{
		if(!isGameFull()){
			synchronized (players) {
				boolean isAgent = false;
				players.put(
						new Long(session),
						new Player(username, out, isAgent));
				connectedPlayerCount++;
				send(new Message("connected"));
			}
		}
	}

	private static Message runRound() {
		// Setup the random generator
		Long seed = new Date().getTime(); 
		Random generator = new Random(seed);

		// Grab all the currently connected players and make them the
		// roundplayers
		roundPlayers = new HashMap<Long, Player>();
		for(Long session : players.keySet()) {
			roundPlayers.put(session, players.get(session));
		}
		ArrayList<Long> turnOrder = new ArrayList<Long>();
		LinkedList<Long> playerQueue = new LinkedList<Long>();
		Physics engine;
		Terrain terrain;
		double wind = generator.nextInt(300);
		if(wind > 150) {
			wind = -(wind-150);
		}
		double gravity = 9.8;


		// Startup a new physics engine for this round
		engine = new Physics(wind, gravity);

		// Create the terrain for the round
		terrain = new Terrain();
		terrain.generate(generator);

		// Place the players on the terrain
		int interval = Math.round((float)800 / (float)roundPlayers.size());
		int i = 0;
		for(Long session : roundPlayers.keySet()) {
			turnOrder.add(session);
			Player player = roundPlayers.get(session);
			int bottom = interval*i;
			int x = generator.nextInt(interval);
			x = x+bottom;
			if(x < 80) x = 80;
			if(x > 720) x = 720;
			Pair pair = terrain.get(x);
			Pair newPair = new Pair(pair.x, pair.y);
			player.setLocation(newPair);
			i++;
		}

		// Randomize turnOrder
		for(i = 0; i < 10; i++) {
			int p1 = generator.nextInt(turnOrder.size());
			int p2 = generator.nextInt(turnOrder.size());
			Long tmp = turnOrder.get(p1);
			turnOrder.set(p1, turnOrder.get(p2));
			turnOrder.set(p2, tmp);
		}

		// Set up playerQueue
		for(Long session : turnOrder) {
			if(roundPlayers.containsKey(session)) {
				playerQueue.add(session);
			}
		}

		String firstPlayer = roundPlayers.get(playerQueue.peek()).getUsername();

		HashMap<String, Pair> playerNames = new HashMap<String, Pair>();
		for(Long session : roundPlayers.keySet()) {
			playerNames.put(
					roundPlayers.get(session).getUsername(),
					roundPlayers.get(session).getLocation());
		}

		// Send the players the new round info
		Message newRoundMsg = new Message("newRound");
		newRoundMsg.set("terrain", terrain);
		newRoundMsg.set("engine", engine);
		newRoundMsg.set("players", playerNames);
		send(newRoundMsg);

		// Get the shots from the players
		while(roundPlayers.size() > 1) {
			// Remove the current player from the queue
			currentPlayer = roundPlayers.get(playerQueue.peek());
			currentSession = playerQueue.remove();

			// Wait for shot info
			Message shotMsg = runTurn();
			// Send to players
			send(shotMsg);
			int power = shotMsg.geti("power");
			int angle = shotMsg.geti("angle");
			// Calculate path
			Pair origin = currentPlayer.getLocation();
			origin.y -= 10;
			ArrayList<Pair> arrayPath = engine.pairs(
					origin,
					power,
					angle,
					terrain);
			Pair end = engine.list().getLast();
			int x = (int)end.x;
			terrain.destroyTerrain(x);

			// Check who's been hit
			Stack<Long> playersToRemove = new Stack<Long>();
			Stack<Pair> tanksToRemove = new Stack<Pair>();
			for(Long playerSession : roundPlayers.keySet()) {
				Player player = roundPlayers.get(playerSession);
				Pair location = player.getLocation();
				int X = (int)location.x;
				if(((x-40) <= X) && (X <= (x+40))) {
					playersToRemove.push(playerSession);
					tanksToRemove.push(location);
				}
			}

			// Remove hit players from playerQueue and roundPlayers
			// Update player scores
			while(!playersToRemove.empty()) {
				Long sessionToRemove = playersToRemove.pop();
				roundPlayers.remove(sessionToRemove);
				playerQueue.remove(sessionToRemove);
			}

			// If playerQueue is empty, reset it
			if(playerQueue.size() <= 0) {
				for(Long session : turnOrder) {
					if(roundPlayers.containsKey(session)) {
						playerQueue.add(session);
					}
				}
			}

			// Sleep for a few seconds while the players render the results
			try { Thread.sleep(1000); }
			catch (InterruptedException e) { e.printStackTrace(); }
		}
		return new Message("endRound");
	}

	private static Message runTurn() {
		int timeElapsed = 0;
		playerNotShot = true;
		send(new Message("signalPlayerTurn"), currentSession);
		while(timeElapsed < turnTimeout && playerNotShot) {
			try { Thread.sleep(1000); }
			catch (InterruptedException e) { e.printStackTrace(); }
			timeElapsed++;
		}
		Message shotMsg = new Message("shot");
		shotMsg.set("power", currentPower);
		shotMsg.set("angle", currentAngle);
		return shotMsg;
	}

	/**
	 * Requests the server to distribute shot information for player with this
	 * session.
	 *
	 * @param  session Session ID passed to the Server for authentication
	 * @param  shot
	 */
	public static void receiveShot(long session, int power, int angle) {
		// return if not this player's turn
		if(currentSession.longValue() != session) return;
		// cut short the wait loop
		playerNotShot = false;
		// set currentPower and currentAngle
		currentPower = power;
		currentAngle = angle;
	}

	/**
	 * Requests the server to remove the player from the game
	 *
	 * @param  session Session ID passed to the Server for authentication
	 */
	public static void disconnect(long session){
		if(sockets.containsKey(session)){
			sockets.remove(session);
		}
		if(players.containsKey(new Long(session))){
			players.remove(new Long(session));
			--connectedPlayerCount;
		}
	}

	public static boolean isGameFull(){
		return maxPlayers == connectedPlayerCount;
	}

	public static boolean isUsernameTaken(String username){
		for(Long session : players.keySet()) {
			Player player = players.get(session);
			if(player.getUsername().equals(username)) return true;
		}
		return false;
	}

	private static void endGame(){
		send(new Message("endGame"));
		System.exit(0);
	}

	private static void send(Object outputObject) {
		for(Long session : players.keySet()) {
			send(outputObject, session);
		}
	}

	private static void send(Object outputObject, Long session) {
		Player player = players.get(session);
		if(player.getConnection() == null) {
			if (player.isAgent()) {
				Message returned = player.send(outputObject);
				if (returned != null) {
					receiveShot(
							session,
							returned.geti("power"),
							returned.geti("angle"));
				}
			} else {
				System.err.println(
						"Error: Player connection is null :Session: "
						+ session);
			}
		} else {
			try {
				player.getConnection().writeObject(outputObject);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	private static void debugDumpPlayers(){
		Set playerSet = players.keySet();
		if(playerSet != null){
			Iterator i = playerSet.iterator();
			while(i.hasNext()){
				Long session = (Long)i.next();
				Player p = players.get(session);
			}
		}
	}

	private static void printUsage(){
		System.out.println(
				"\n\nUsage: Server PORT MAX-PLAYERS" +
				" NUM-ROUNDS TURN-TIMEOUT AGENTS" +
				"\n PORT\t Port the server will listen on for client requests" +
				"\n MAX-PLAYERS\t Maximum number of players for the game" +
				"\n NUM-ROUNDS\t The number of rounds for game." +
				"\n TURN-TIMEOUT\t Time limit for player to finish turn" +
				"(in seconds)" +
				"\n AGENTS\t Number of agents to use" +
				"\nExample: java Server 12121 5 5 10");
	}
}
